/**
 * Copyright 2009 cestum.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.cestum.cave.presenter {
	import com.google.protobuf.Message;
	
	import mx.controls.Alert;
	
	import org.cestum.cave.logger.Logger;
	import org.cestum.cave.presenter.rpc.CaveChannel;
	import org.cestum.cave.presenter.rpc.CaveRPCController;
	import org.cestum.cave.presenter.wave.ClientWaveView;
	import org.cestum.cave.presenter.wave.HashedVersion;
	import org.cestum.cave.presenter.wave.WaveletOperationSerializer;
	import org.cestum.cave.presenter.wave.cursors.DocSizeFinder;
	import org.cestum.cave.presenter.wave.cursors.FindLastLineCursor;
	import org.cestum.cave.presenter.wave.cursors.UndoLastLineDocCursor;
	import org.cestum.cave.presenter.wave.cursors.caveRenderUtils;
	import org.cestum.cave.presenter.wave.utils.HashedVersionZeroFactoryImpl;
	import org.cestum.cave.presenter.wave.utils.RandomBase64Generator;
	import org.cestum.cave.presenter.wave.utils.RandomIdGenerator;
	import org.cestum.cave.ui.CaveView;
	import org.cestum.cave.ui.events.CaveSocketConnEvent;
	import org.cestum.cave.ui.events.SettingsChangedEvent;
	import org.goverla.collections.ArrayList;
	import org.goverla.collections.HashMap;
	import org.goverla.collections.ListCollectionViewIterator;
	import org.waveprotocol.wave.examples.fedone.waveserver.ProtocolOpenRequest;
	import org.waveprotocol.wave.examples.fedone.waveserver.ProtocolSubmitRequest;
	import org.waveprotocol.wave.examples.fedone.waveserver.ProtocolSubmitRequestBuilder;
	import org.waveprotocol.wave.examples.fedone.waveserver.ProtocolSubmitResponse;
	import org.waveprotocol.wave.examples.fedone.waveserver.ProtocolWaveletUpdate;
	import org.waveprotocol.wave.model.document.operation.BufferedDocOp;
	import org.waveprotocol.wave.model.document.operation.DocumentOperation;
	import org.waveprotocol.wave.model.document.operation.impl.AttributesImpl;
	import org.waveprotocol.wave.model.document.operation.impl.DocOpBuilder;
	import org.waveprotocol.wave.model.document.operation.impl.InitializationCursorAdapter;
	import org.waveprotocol.wave.model.id.IdConstants;
	import org.waveprotocol.wave.model.id.IdURIEncoderDecoder;
	import org.waveprotocol.wave.model.id.WaveId;
	import org.waveprotocol.wave.model.id.WaveletId;
	import org.waveprotocol.wave.model.id.WaveletName;
	import org.waveprotocol.wave.model.operation.wave.AddParticipant;
	import org.waveprotocol.wave.model.operation.wave.WaveletDelta;
	import org.waveprotocol.wave.model.operation.wave.WaveletDocumentOperation;
	import org.waveprotocol.wave.model.operation.wave.WaveletOperation;
	import org.waveprotocol.wave.model.util.Pair;
	import org.waveprotocol.wave.model.util.URLEncoderDecoderBasedPercentEncoderDecoder;
	import org.waveprotocol.wave.model.wave.ParticipantId;
	import org.waveprotocol.wave.model.wave.data.WaveletData;
	import org.waveprotocol.wave.protocol.ProtocolWaveletDelta;
	

	/**
	 * Does the work of connecting to a channel, wave operations etc
	 * 
	 * It contains
	 * 		1. Particpant
	 * 		2. IDGenerator
	 * 		3. codec
	 * 		4. InboxList
	 *      5. Wavelets List
	 * 		6. Participant List
	 * 		7. Channel -> Socket
	 * 
	 */
	public class CavePresenter {
		
		[Inject(id="view")]
		public var thisView:IViewEnabler;
		[Inject(id="channel")]
		public var thisChannel:CaveChannel;
		
		//Presenter Specific components
		private var codec:IdURIEncoderDecoder;
		private var waves:HashMap;
		
		//actually blips!
		private var waveletList:ArrayList;
		private var participantList:ArrayList;
		private var idGenerator:RandomIdGenerator;
		private var INDEX_WAVE_ID:WaveId = new WaveId("", "indexwave");
		private var thisParticipant:ParticipantId;
		private var waveControllers:HashMap;
		private var inboxList:ArrayList;
		private var openedWave:WaveletData;
		private var base64Generator:RandomBase64Generator;
		private var  EMPTY_ATTRS:AttributesImpl = new AttributesImpl(null);
		
		public function CavePresenter(){}
		/**
		 * Initialize presenter specific details
		 */
		public function init():void {
			//initalize components
			waves = new HashMap();
			waveletList = new ArrayList();
			participantList = new ArrayList();
			inboxList = new ArrayList();
			codec = new IdURIEncoderDecoder(new URLEncoderDecoderBasedPercentEncoderDecoder());
			thisParticipant = new ParticipantId(thisView.getParticipant().name);
			idGenerator = new RandomIdGenerator(thisParticipant.getDomain());
			waveControllers = new HashMap();
			base64Generator = new RandomBase64Generator();
			//Initialize channels
			thisChannel.init();
			CaveView(thisView).myWaveList.dataProvider = inboxList;
		}
		
		
		[MessageHandler(selector="settingsChanged")]
		public function settingsChanged(e:SettingsChangedEvent):void {
			//distribute other changes
			thisChannel.changeHostPort(e.params.hostName, e.params.port);			
		}
		
		[MessageHandler(selector="SOCKET_CONNECTED")]
		public function socketConnected(e:CaveSocketConnEvent):void {
			refreshWaveData();
		}
		public function refreshWaveData():void {
			Logger.log("Receiving updates...");
			waves = new HashMap();
			waveletList = new ArrayList();
			participantList = new ArrayList();
			inboxList = new ArrayList();
			openWave(INDEX_WAVE_ID, "");
		}
		
		/**
		 * Open the given wave and get updates on that wave...
		 **/		
		private function openWave(waveId:WaveId, prefix:String):void {
			//check if this wave already exists in our list, else add it...
			var id:String = waveId.serialise();
			if (!waves.containsKey(id)) {
				createWave(waveId);
			}
			//Protocol Open Request
			var op:ProtocolOpenRequest = new ProtocolOpenRequest();		
			op.participantId = thisParticipant.getAddress();
			op.waveId = id;
			op.waveletIdPrefix.addItem(prefix);
			var up:ProtocolWaveletUpdate = ProtocolWaveletUpdate.DEFAULT_VERSION;
			var controller:CaveRPCController=thisChannel.callMethod(op, up, function(msg:Message) {
				receiveWaveletUpdate(msg as ProtocolWaveletUpdate);
			});
			waveControllers.addItem(id, controller);
		}
		
		
		private function sendWaveletDelta(waveletName:WaveletName, delta:WaveletDelta):void {
			var submitRequest:ProtocolSubmitRequestBuilder  = ProtocolSubmitRequest.newBuilder();
			var id:String = waveletName.waveId.serialise();
			try {
				submitRequest.setWaveletName(new String(codec.waveletNameToURI(waveletName)));
			} catch (e) {
				throw new Error(e);
			}
			var wave:ClientWaveView = waves.getItem(id) as ClientWaveView;
			submitRequest.setDelta(ProtocolWaveletDelta(WaveletOperationSerializer.serialize(
				delta,wave.getWaveletVersion(waveletName.waveletId))));
			var up:ProtocolSubmitResponse = ProtocolSubmitResponse.getDefaultInstance();
			
			var controller:CaveRPCController=thisChannel.callMethod(submitRequest.build(),up, function(msg:Message) {
				trace("do nothing...");
			});
			waveControllers.addItem(id, controller);
		}
		
		/**
		 * Creates a new, empty wave view and stores it in {@code waves}.
		 */
		private function createWave(waveId:WaveId):ClientWaveView {
			var wave:ClientWaveView = new ClientWaveView(new HashedVersionZeroFactoryImpl(), waveId);
			waves.addItem(waveId.serialise(), wave);
			return wave;
		}
		

		public function receiveWaveletUpdate(waveletUpdate:ProtocolWaveletUpdate):void {
			trace("callbacked!");
			
			var deltas:ArrayList = waveletUpdate._appliedDelta;
			var waveletName:WaveletName;
			try {
				waveletName = codec.uriToWaveletName(waveletUpdate.waveletName);
			} catch (e) {
				throw new Error(e);
			}
			
			var wave:ClientWaveView = waves.getItem(waveletName.waveId.serialise()) as ClientWaveView;
			if (wave == null) {
				// The wave view should always be present, since openWave adds them immediately
				throw new Error("Received update on absent waveId " + waveletName.waveId);
			}
			
			var wavelet:WaveletData = wave.getWavelet(waveletName.waveletId);
			if (wavelet == null) {
				wavelet = wave.createWavelet(waveletName.waveletId);
			}
			
			//		waveletOperationListeners:ArrayList  = this.waveletOperationListeners;
			//		for (WaveletOperationListener listener : waveletOperationListeners) {
			//			listener.onDeltaSequenceStart(wavelet);
			//		}
			
			// Apply operations to the wavelet
			var successfulOps:ArrayList = new ArrayList();
			var itr:ListCollectionViewIterator = deltas.createIterator();
			while(itr.hasNext()) {
				var protobufDelta:ProtocolWaveletDelta = itr.next() as ProtocolWaveletDelta;
				var deltaAndVersion:Pair =
					WaveletOperationSerializer.deserialize(protobufDelta) as Pair;
				var ops:ArrayList = WaveletDelta(deltaAndVersion.first).getOperations();
				
				var size:int = ops.length;
				for (var i:int =0; i < size; i++) {
					var op:WaveletOperation =  ops.getItemAt(i) as WaveletOperation;
					try {
						op.apply(wavelet);
						successfulOps.addItem(Pair.of(protobufDelta.author_, op));
					} catch (e:Error) {
						Logger.log("Exception while applying operation: " + e.getStackTrace());
//						throw e;
					}
				}
			}
			wave.setWaveletVersion(
				waveletName.waveletId, 
				WaveletOperationSerializer.deserialize(waveletUpdate.resultingVersion) as HashedVersion);
			//		// Notify listeners separately to avoid them operating on invalid wavelet state
			//		// TODO: take this out of the network thread
			//		var size1:int = successfulOps.length;
			//		for (var i:int =0; i< size1; i++) {
			//			var authorAndOp:Pair = successfulOps.getItemAt(i) as Pair;
			//			//    	 for (Pair authorAndOp : successfulOps) {
			////			notifyWaveletOperationListeners((String)authorAndOp.first, wavelet, (WaveletOperation)authorAndOp.second);
			//		}
			//		
			// If we have been removed from this wavelet then remove the data too, since if we're re-added
			// then the deltas will come from version 0, not the latest version we've seen
			//		if (!wavelet.getParticipants().contains(getUserId())) {
			//			wave.removeWavelet(waveletName.waveletId);
			//		}
			//		
			//		// If it was an update to the index wave, might need to open/close some more waves
			if (wave.getWaveId().equals(INDEX_WAVE_ID)) {
				syncWithIndexWave(wave);
			}
			//		
			//		for (WaveletOperationListener listener : waveletOperationListeners) {
			//			listener.onDeltaSequenceEnd(wavelet);
			//		}
			//		
			//end of delta sequence
			updateInboxAndWaves(wavelet);
			
		}
		/**
		 * Updates the inbox list.
		 */
		function updateInboxAndWaves(wavelet:WaveletData):void {
			var size:int = inboxList.length;
			var id:String = wavelet.getWaveletName().waveId.getId();
			if (id != "indexwave") {
				//update the inbox...
				openedWave = wavelet;
				updateWaveletsView(wavelet);
			} else {
				id  = wavelet.getWaveletName().waveletId.getId();
				var temp:String;
				for (var x:int=0; x<size; x++) {
					//To costly
					//ObjectUtil.compare(wavelet, WaveletData(inboxList.getItemAt(x));
					temp = WaveletData(inboxList.getItemAt(x)).getWaveletName().waveletId.getId()
					if (temp == id) {
						inboxList.setItemAt(wavelet,x);
						//					if (x == this.view.openWaveidx)
						//						openSelectedWave();
						return;
					}
				}
				inboxList.addFirst(wavelet);
				//openSelectedWave();
			}
			CaveView(thisView).myWaveList.dataProvider = inboxList;
		}
		
		
		[MessageHandler(selector="waveSelected")]
		public function openSelectedWave():void {
			var wd:WaveletData = inboxList.getItemAt(thisView.getOpenWaveIndex()) as WaveletData;
			//		var waveId:WaveId = wd.getWaveletName().waveId;
			var waveId1:WaveId = WaveId.deserialise(wd.getWaveletName().waveletId.serialise());
			//		trace(waveId1);
			//		var wave:ClientWaveView = waves.getItem(waveId.getDomain().concat(":").concat(waveId.getId())) as ClientWaveView;
			var rootWavelet:WaveletId = new WaveletId(waveId1.getDomain(), IdConstants.CONVERSATION_ROOT_WAVELET);
			openWave(waveId1, rootWavelet.serialise());
		}
		
		private function syncWithIndexWave(wave:ClientWaveView):void {
			
		}
		
		/**
		 * Update Blips on Selected wavelet
		 */
		public function updateWaveletsView(wd:WaveletData):void {
			var docs:HashMap = wd.getDocuments();
			if (docs == null)
				return;
			var manifest:BufferedDocOp = docs.getItem("conversation") as BufferedDocOp;		
			thisView.render(manifest, docs);
			//Update view!
			thisView.updateParticipants(wd.getParticipants().toArray());
		}
//
//		//Creates a Blip on the view
//		public function createBlip(id:String, parentObj:Object=null):void {
//			thisView.createBlip(id,parentObj);
//		}
//		//Creates a Blip on the view
//		public function updateBlip(id:String, chars:String):void {
//			thisView.updateBlip(id, chars);
//		}
//		private function renderManifest(manifest:BufferedDocOp, docs:HashMap, lines:ArrayList, currentLine:StringBuilder):void {
//			if (manifest == null)
//				return;
//			thisView.getBlipsDisplay().updateCursor(docs, manifest);
//			manifest.apply(new InitializationCursorAdapter(thisView.getBlipsDisplay()));
//		}
//		
//		public function renderDocument(document:BufferedDocOp, blipId:String, lines:ArrayList, currentLine:StringBuilder):void {
//			if (document == null)
//				return;
//			document.apply(new InitializationCursorAdapter(new cDocumentInitCursor(this, document, blipId, currentLine, lines)));
//		}
		
		public function sendAppendMutation(text:String):void {
			if (text.length == 0) {
				throw new Error("Cannot append a empty String");
			} else if (openedWave != null) {
				//NEW CODE
				var docId:String = "b+" + base64Generator.next(6); // TODO: constant
				var openDoc:BufferedDocOp = getNewDocument(docId);
				var docOp:DocOpBuilder = new DocOpBuilder();
				
				
				
				//      docOp.elementStart(
				//          ConsoleUtils.CONTRIBUTOR,
				//          new AttributesImpl(
				//              ImmutableMap.of(ConsoleUtils.CONTRIBUTOR_NAME,
				//                              backend.getUserId().getAddress())));
				docOp.elementStart(caveRenderUtils.BODY, EMPTY_ATTRS);
				docOp.elementStart(caveRenderUtils.LINE, EMPTY_ATTRS);
				docOp.elementEnd(); // line
				docOp.characters(text);
				docOp.elementEnd(); // body
				//      docOp.elementEnd(); // contributor
				
				// Get the conversation.
				var manifestDocument:BufferedDocOp = getManifestDocument();
				var manifestDocOp:DocOpBuilder = new DocOpBuilder();
				var docSize:int = findDocumentSize(manifestDocument);
				manifestDocOp.retain(docSize - 1);
				var tempMap:HashMap = new HashMap();
				tempMap.addItem(caveRenderUtils.BLIP_ID, docId);
				manifestDocOp.elementStart("blip", AttributesImpl.of(tempMap));
				manifestDocOp.elementEnd();
				manifestDocOp.retain(1); // </conversation>
				
				var operations:ArrayList = new ArrayList();
				operations.addItem(new WaveletDocumentOperation(docId, new DocOpBuilder().build()));
				operations.addItem(new WaveletDocumentOperation(docId, docOp.build()));
				operations.addItem(new WaveletDocumentOperation(
					caveRenderUtils.MANIFEST_DOCUMENT_ID,
					manifestDocOp.build()));
				
				sendWaveletDelta(openedWave.getWaveletName(),	new WaveletDelta(thisParticipant,operations));
			} 
		}
		/**
		 * @return new document, or null if no wave is open
		 */
		private function getNewDocument(docId:String):BufferedDocOp {
			return openedWave == null ? null : openedWave.getDocuments().getItem(docId) as BufferedDocOp;
		}
		
		
		/**
		 * @return open document, or null if no wave is open or main document doesn't
		 *         exist
		 */
		private function getManifestDocument():BufferedDocOp {
			return openedWave == null ? null :  openedWave.getDocuments().getItem(caveRenderUtils.MANIFEST_DOCUMENT_ID) as BufferedDocOp;
		}
		
		private static function findDocumentSize(doc:DocumentOperation):int {		
			var d:DocSizeFinder = new DocSizeFinder();
			doc.apply(new InitializationCursorAdapter(d));
			return d.getSize();
		}
		
		private function sendWaveletOperation(waveletName:WaveletName, op:WaveletOperation):void {
			var opList:ArrayList = new ArrayList();
			opList.addItem(op);
			sendWaveletDelta(waveletName, new WaveletDelta(thisParticipant, opList));
		}
		
		public function undoLastLine():void {
			if (openedWave != null) {
				var openDoc:BufferedDocOp = openedWave.getDocuments().getItem(caveRenderUtils.MAIN_DOCUMENT_ID) as BufferedDocOp;
				var d:FindLastLineCursor = new FindLastLineCursor(thisParticipant.getAddress());
				openDoc.apply(new InitializationCursorAdapter(d));
				if (d.getLastLine() > 0) {
					var undoDocCursor:UndoLastLineDocCursor = new UndoLastLineDocCursor(d.getLastLine());
					openDoc.apply(new InitializationCursorAdapter(undoDocCursor));
					var undoOp:WaveletDocumentOperation = 
						new WaveletDocumentOperation(
							caveRenderUtils.MAIN_DOCUMENT_ID,undoDocCursor.getDocOp());
					sendWaveletOperation(openedWave.getWaveletName(), undoOp); 
				}
			} 
		}
		
		public function deleteWavelet():void {
			Alert.show("Feature Not implemented yet");
		}
		//create Conversation
		public function addWavelet():void {
			//get IDGenerator
			var newWaveId:WaveId =  idGenerator.newWaveId();
			var waveView:ClientWaveView = createWave(newWaveId);
			var convRoot:WaveletData = waveView.createWavelet(idGenerator.newConversationRootWaveletId());
			var addOp:AddParticipant = new AddParticipant(thisParticipant);
			var manifestOp:WaveletDocumentOperation = new WaveletDocumentOperation(
				caveRenderUtils.CONVERSATION, new DocOpBuilder().elementStart("conversation", new AttributesImpl(null)).elementEnd().build());
			var tempOp:ArrayList = new ArrayList();
			tempOp.addItem(addOp);
			tempOp.addItem(manifestOp);
			sendWaveletDelta(convRoot.getWaveletName(), new WaveletDelta(thisParticipant, tempOp));
			//		return waveView;
		}
		
		public function addParticipant(name:String):void {
			var addId:ParticipantId = new ParticipantId(name);
			// Don't send an invalid op, although the server should be robust enough to deal with it
			var al:ArrayList = openedWave.getParticipants();
			var size:int = al.length;
			for (var i:int =0 ; i< size; i++) {
				var temp:ParticipantId = al.getItemAt(i) as ParticipantId;
				if (temp.getAddress() == name) {
					Alert.show("Participant already exists.");
					return;
				}
			}
			sendWaveletOperation(openedWave.getWaveletName(), new AddParticipant(addId));
		}
		public function removeParticipant():void {
			Alert.show("Feature Not implemented yet");
		}
		
		public function getOpenWave():WaveletData {
			return openedWave;
		}
		
//		public function saveConnectionParams(host:String, port:String):void {
//			if (host != null && host.replace(" ","") != "") {
//				this.host = host;
//			}
//			if (port != null && port.replace(" ","") != "") {
//				this.port = new Number(port);
//			}
//			Logger.log("Connection settings changed. Please refresh Connection");
//		}

	}
}